《Effective Java》第十一章:序列化
“将一个对象编码成一个字节流”,这个过程称为对象序列化,相反的过程称为反序列化。
第74条:谨慎地实现Serializable接口
只要添加implements Serializable
,就可以使一个类可被序列化,写法上虽然看上去很简单,但是如果你认为真的这么简单的话就会付出很大的代价:
- 一旦一个可序列化类被发布,就大大降低了“改变这个类的实现”的灵活性。
- 一旦一个类实现
Serializable
接口,它的字节流编码就变成了导出API的一部分,一旦这个类被广泛使用,往往必须永远支持这种序列化形式。 - 如果接受了默认的序列化形式,并且以后又要改变这个类的内部实现,结果可能会导致序列化形式不兼容
- 序列化会使类的演变受到限制,这种限制与序列化版本UID有关,每个可序列化的类都有一个唯一标识符
serialVersionUID
,如果没有指定,则系统会根据类的具体实现来进行自动生成,所以你修改了类,并且没有显示的指定UID,那么兼容性就会被破坏。
- 一旦一个类实现
- 它增加了Bug和安全漏洞的可能性
序列化机制是一种语言之外的对象创建机制,所以反序列化是一个“隐藏的构造器”,具体与其他构造器相同的特点,所以反序列化过程也必须保证有构造器建立起来的约束关系,并且不允许攻击者在访问构造器过程中的内部对象,所以默认的反序列化机制很容易是对象约束遭到破坏,以及遭受非法的访问。 - 随着类发行新的版本,相关测试的负担也增加了
新的版本发布之后,要检查是否可以“在新版本中序列化一个实例,然后在旧版本中依然可以反序列化”,这个测试除了二进制兼容性以外,还要测试语义兼容性。所有测试的难度都是相当大啊!
下面是关于序列化类的操作:
- 如果一个了类将要加入某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化,那么实现
Serializable
这个接口就是非常有必要的。 - 还有为了继承而设计的类尽量少的去实现
Serializable
接口,用户的接口也应该尽可能烧得继承Serializable
接口,不然会对实现这些接口或者类的程序猿增加很多负担 实现一个带有可序列化实例的类时,应该要注意类的约束条件,并且要实现
1
2
3private void readObjectNoData() throws InvalidObjectException{
throw new InvalidObjectException("stream data is required");
}对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器
- 内部类不应该实现
Serializable
简而言之,千万不要认为实现Serializable
很容易,里面的坑多着呢。
第75条:考虑使用自定义的序列化形式
一个对象的默认序列化使将该对象进行物理表示,也就是说,默认序列化描述了该对象内部所包含的数据,以及每一个可以从这个对象到达其他对象的内部数据。
而对于一个对象来说,理想的序列化应该只包含该对象索比表示的逻辑数据,而逻辑数据和物理数据是应该相互独立的。
当对象的物理表示等同于它的路基表示是,使用默认的序列化是合理的:1
2
3
4
5public class Name implements Serializable{
private final String lastName;
private final String firstName;
private final String middelName;
}
类似这样的实体类,物理内容的这三个字段也可以很精确的反应它的逻辑内容。
来看看下面的序列化类:1
2
3
4
5
6
7
8
9
10public final class StringList implements Serializable{
private int size=0;
private Entry head = null;
private class Entry implements Serializable{
String data;
Entry next;
Entry previous;
}
}
从逻辑意义上讲,这个类表示一个字符串的序列,但是从物理意义上讲,把该序列化表示为一个双向链表。所以此时如果使用默认的序列化,那将会镜像除链表中所有的项以及这些项之间的所有双向链表。。-_-,大工程啊
当一个类的物理表示与它的逻辑内容有区别时,使用默认序列化将会有以下4个缺点:
- 它使这个类的导出API永远的束缚在该类的内容(
StringList.Entry
也会成为公有API的一部分) - 会消耗很多的空间(因为会维护一个双向链表)
- 会消耗很多时间(遍历啊)
- 会引起栈溢出(递归啊)
其实类似这种类编写它的自定义序列化需求并不是很麻烦:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34public final class StringList implements Serializable{
//添加“易变”标志防止默认序列化
private transient int size=0;
private transient Entry head = null;
private class Entry{
String data;
Entry next;
Entry previous;
}
public final void add(String s){
//TO DO
}
private void writeObject(ObjectOutputStream s) throws IOException
{
s.defaultWriteObject();
s.writeInt(size);
//在这里序列化
for(Entry e=head;e != null ; e=e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s)
throws ClassNotFoundException,IOException
{
s.defaultReadObject();
//在这里进行反序列化
int numElements= s.readInt();
for(int i=0;i<numElements;i++)
add((String)s.readObject());
}
}
这里调用默认的readObject()
和writeObject()
可以影响全类的序列化形式,极大的增强灵活性。
还有类似key-value
的散列表也不适合使用默认的序列化,因为在不同的JVM中最终形成的散列位置可能会不一样。
切记不管使用什么序列化方式,建议都显示的加上序列的唯一版本UID
总而言之,当默认的序列化能合理的描述逻辑内容时,使用默认序列化就好了,否则建议使用自定义的序列化^_^
第76条:保护性的编写readObject方法
针对第39条的日期类,根据上一条的指导,貌似使用默认的序列化也是蛮合理的,增加implements Serializable
即可。但是如果真的这么做,那么这个类将不再保证它的关键约束了。
因为readObject()
方法相当于是一个接受字节流的构造函数,那如果有人伪造了这个字节流的话,反序列化出来的对象时相当危险的。还有一个信号就是反序列化出来对象还可以随意被改动,因为默认的序列化并没有使用保护性拷贝,所以如果增对该日期类实现自己的序列化的化,可以这么干:1
2
3
4
5
6
7
8
9
10
11private void readObject(ObjectInputStream s)
throws IOException,ClassNotFoundException
{
s.defaultReadObject();
start=new Date(start.getTime());//进行保护兴拷贝
end=new Date(end.getTime());
if(start.compareTo(end)>0)//进行安全性检查
throw new InvalidObjectException(start +"after"+ end);
}
下面是编写readObject
方法的指导方针:
- 对于对象引用域必须保持私有的类,要保护性得拷贝这个域中的每个对象。
- 对于任何约束条件,如果检查失败,则应该抛出一个
InvalidObjectException
异常。 - 如果整个对象图在被反序列化之后必须进行验证,就应该使用
ObjectInputValidation
接口 - 无论是直接方式还是间接方法,都不要调用类中任何可被覆盖的方法
总而言之,当你编写readObject
方法的时候,都要想:你正在编写一个公有的构造器,无论给他传递什么字节流,都必须产生一个有效的实例。
第77条:对于实例控制,枚举类型优于readResolve
一般的单例类,如果添加了implements Serializable
之后,它就不再是一个单例,因为反序列化可以看做是另一个构造器,此时你就需要使用readResolve()
方法,1
2
3
4private Object readResolve()
{
return INSTANCE;
}
直接返回这个单例就好了,但是注意的是这个单例需要用transient
来标记
当然说了这么做,然而其实这并没什么卵用,这种方法还不如使用枚举单例 -_-,可以参考第3条,它写得简单,用的省心。
第78条:考虑用序列化代理代替序列化实例
序列化代理模式可以解决普通类实现序列化时带来的各种副作用。
序列化代理模式非常简单:
- 为可序列化类设计一个私有的静态嵌套类
- 为该嵌套类添加一个构造器,实现外围类的参数的复制
3, 这个嵌套类中需要添加一个readResolve()
方法进行外围类的返回 - 在外围类中添加
writeReplace()
方法进行外围类的复制 - 在外围类中还要添加一个
readObject
防止被攻击
根据上述指导,看下EnumSet
源码中的序列化代理类的实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51/**
* This class is used to serialize all EnumSet instances, regardless of
* implementation type. It captures their "logical contents" and they
* are reconstructed using public static factories. This is necessary
* to ensure that the existence of a particular implementation type is
* an implementation detail.
*
* @serial include
*/
private static class SerializationProxy <E extends Enum<E>>
implements java.io.Serializable
{
/**
* The element type of this enum set.
*
* @serial
*/
private final Class<E> elementType;
/**
* The elements contained in this enum set.
*
* @serial
*/
private final Enum[] elements;
SerializationProxy(EnumSet<E> set) {
elementType = set.elementType;//把数据拷贝进来
elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY);
}
private Object readResolve() {
EnumSet<E> result = EnumSet.noneOf(elementType);
for (Enum e : elements)
result.add((E)e);
return result;//实例化Enumset
}
private static final long serialVersionUID = 362491234563181265L;
}
Object writeReplace() {//代理模式的入口
return new SerializationProxy<>(this);
}
// readObject method for the serialization proxy pattern
// See Effective Java, Second Ed., Item 78.
private void readObject(java.io.ObjectInputStream stream)
throws java.io.InvalidObjectException {//防止被攻击 竟然注释和Effective Java有关
throw new java.io.InvalidObjectException("Proxy required");
}
总而言之,推荐使用序列化代理模式!!!^_^
总结
真不容易,这本书终于看完了,这段时间因为工作日要上班,并且还要被老板催着科研论文,真心蛋疼~所以看书的进度还是比较慢啊。
不够回顾本书,里面还是提出了蛮多非常经典的建议的,感谢本书,感谢作者。^_^